/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package nsip.components;

import nsip.components.ds.PriceTrackerDataBase;
import nsip.util.Const;
import java.util.logging.Level;
import nsip.model.ProductPartInfo;
import nsip.model.Item;
import java.util.Map;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;
import nsip.components.ds.Catalog;
import nsip.components.ds.InventoryDataBase;
import nsip.components.ds.OrderDataBase;
import nsip.components.ds.StatisticDataBase;
import nsip.components.ds.StatisticType;
import nsip.transport.NsipMessageSucker;
import nsip.model.ProductPriceInfo;
import nsip.model.messages.NewDay;
import nsip.model.DataRecord;
import nsip.model.ItemStatus;
import nsip.model.ItemType;
import nsip.transport.NsipTransport;
import nsip.util.Utils;
import se.sics.tasim.props.ActiveOrders;
import se.sics.tasim.props.BOMBundle;
import se.sics.tasim.props.BankStatus;
import se.sics.tasim.props.ComponentCatalog;
import se.sics.tasim.props.FactoryStatus;
import se.sics.tasim.props.PriceReport;
import se.sics.tasim.props.StartInfo;

import static nsip.components.ds.StatisticType.*;

/**
 *
 * @author nico.rehwaldt
 */
public class InformationSystem implements NsipMessageSucker {

    private static final Logger logger = Logger.getLogger("InformationSystem");
    
    private Map<Integer, ProductPriceInfo> priceInfos;
    
    private StartInfo gameInfo;

    private int daysBeforeVoid;
    private BankStatus bankStatus;

    private int currentDay = 0;
    
    private NsipTransport transport;

    private final InventoryDataBase inventory;
    private final StatisticDataBase<StatisticType> statistics;
    private final OrderDataBase customerOrderDB;
    private final OrderDataBase supplierOrderDB;

    private final PriceTrackerDataBase priceTracker;
    private final Catalog catalog;

    public InformationSystem(NsipTransport transport) {
        
        this.catalog = new Catalog();
        this.priceInfos = new HashMap<Integer, ProductPriceInfo>();
        this.transport = transport;
        this.priceTracker = new PriceTrackerDataBase();
        
        this.inventory = new InventoryDataBase();
        this.statistics = new StatisticDataBase<StatisticType>(StatisticType.class);
        
        this.customerOrderDB = new OrderDataBase("CUSTOMER", transport, statistics);
        this.supplierOrderDB = new OrderDataBase("SUPPLIER", transport, statistics);
    }

    /**
     * @return the gameInfo
     */
    public StartInfo getGameInfo() {
        return gameInfo;
    }

    /**
     * Get rhe price info for a product
     * @param productID
     * @return
     */
    public ProductPriceInfo getProductPriceInfo(int productID) {
        return priceInfos.get(productID);
    }

    /**
     * Returns the customer order database
     * @return
     */
    public OrderDataBase getCustomerDB() {
        return customerOrderDB;
    }

    /**
     * Return the supplier order database
     * @return
     */
    public OrderDataBase getSupplierDB() {
        return supplierOrderDB;
    }

    /**
     * Return the inventory for tomorrow
     * @return
     */
    public InventoryDataBase getInventory() {
        return inventory;
    }

    /**
     * Returns the price tracer of the information system
     * @return
     */
    public PriceTrackerDataBase getPriceTracker() {
        return priceTracker;
    }
    
    public void cancelCustomerOrder(Item i) {
        Item order = customerOrderDB.get(ItemType.ORDER, i.getId());
        if (order == null) {
            logger.info("Request to cancel non existing order ignored.");
            return;
        }

        order.setStatus(ItemStatus.CANCELED);

        // The components for the canceled order are now available to be
        // used in other orders.
        ProductPartInfo[] components = catalog.getInfo(order.getProductID())
                                              .getComponents();

        int quantity = order.getQuantity();

        for (ProductPartInfo part: components) {
            inventory.demanded().remove(
                    part.getId(),
                    quantity, order.getDueDate() - Const.DAYS_TO_PRODUCE);
        }
    }

    /**
     * Restores orders in the databases. This must also built up all sorts of
     * other things.
     * 
     * @param orders
     */
    private void restoreOrders(ActiveOrders orders) {
        int numCustomerOrders = 0;
        int numSupplierOrders = 0;

        Map<Integer, Item> cOrderMap = customerOrderDB.orderMap();
        
        for (int i = 0, n = orders.getCustomerOrderCount(); i < n; i++) {
            int orderID = orders.getCustomerOrderID(i);
            if (!cOrderMap.containsKey(orderID)) {
                // The order was not already known
                Item order = new Item(new DataRecord(
                        orderID,
                        orders.getCustomerProductID(i),
                        ItemType.ORDER,
                        ItemStatus.OPEN,
                        orders.getCustomerQuantity(i),
                        orders.getCustomerDueDate(i),
                        orders.getCustomerPenalty(i),
                        orders.getCustomerUnitPrice(i), 
                        orders.getCustomerAddress(i)
                ));

                cOrderMap.put(orderID, order);
                numCustomerOrders++;
            }
        }

        Map<Integer, Item> sOrderMap = customerOrderDB.orderMap();

        for (int i = 0, n = orders.getSupplierOrderCount(); i < n; i++) {
            int orderID = orders.getSupplierOrderID(i);
            if (!sOrderMap.containsKey(orderID)) {
                // The order was not already known
                Item order = new Item(new DataRecord(
                        orderID,
                        orders.getSupplierProductID(i),
                        ItemType.ORDER,
                        ItemStatus.OPEN,
                        orders.getSupplierQuantity(i),
                        orders.getSupplierDueDate(i),
                        orders.getSupplierPenalty(i),
                        orders.getSupplierUnitPrice(i),
                        orders.getSupplierAddress(i)
                ));
                
                sOrderMap.put(orderID, order);
                numSupplierOrders++;
            }
        }

        if (numCustomerOrders > 0 || numSupplierOrders > 0) {
            logger.log(Level.WARNING,
                    "Restored {0} active customer orders and {1} supplier orders",
                    new Object[]{ numCustomerOrders, numSupplierOrders });
        }
    }

    /**
     * Update the price info with the data contained in the argument
     * @param info
     */
    public void updatePriceInfo(List<ProductPriceInfo> info) {
        for (ProductPriceInfo pi: info) {
            this.priceInfos.put(pi.getProductID(), pi);
        }
    }

    public void receive(Object message) {
        
        if (message instanceof ComponentCatalog) {
            catalog.setComponentCatalog((ComponentCatalog) message);
        } else
            
        if (message instanceof BOMBundle) {
            catalog.setBOMBundle((BOMBundle) message);
        } else

        if (message instanceof StartInfo) {
            // Start information for manufacturers
            gameInfo = (StartInfo) message;
            daysBeforeVoid = getGameInfo().getAttributeAsInt("customer.daysBeforeVoid", 0);
        } else
        
        if (message instanceof FactoryStatus) {
            FactoryStatus status = (FactoryStatus) message;
            inventory.updateFromStatus(status);
            statistics.getMetric(FACTORY_UTILIZATION).update(status.getUtilization());
        } else
        
        if (message instanceof BankStatus) {
            bankStatus = (BankStatus) message;
        } else

        if (message instanceof PriceReport) {
            List<ProductPriceInfo> infos = Utils.toPriceInfoList((PriceReport) message);
            updatePriceInfo(infos);
        } else

        if (message instanceof ActiveOrders) {
            // Report of active orders from the server
            ActiveOrders orders = (ActiveOrders) message;
            if (orders.getCurrentDate() > currentDay) {
                currentDay = orders.getCurrentDate();
            }

            restoreOrders(orders);
        } else

        if (message instanceof NewDay) {
            currentDay = ((NewDay) message).getDate();
            
            logger.info("-------------------------------------");
//            logger.info("SUPL_OFFER         " + statisticDB.counters().get(SUPPLIER_OFFER));
//            logger.info("SUPL_RFQS          " + statisticDB.counters().get(SUPPLIER_RFQS));
//            logger.info("SUPL_ORDER         " + statisticDB.counters().get(SUPPLIER_ORDERS));
//            logger.info("INC_SUPL_OFFER     " + statisticDB.counters().get(INCOMPLETE_SUPPLIER_OFFER));
            logger.info(" [Inventory] " + inventory.tomorrow());
            logger.info(" [Demand]    " + inventory.demanded());
            logger.info(" [Statistics] " + statistics);
            logger.info("-------------------------------------");


            priceTracker.updateForNextDay(currentDay);
            inventory.updateForNextDay(currentDay);
        }
    }
    
    public Collection<Class> getMessageInterests() {
        return Arrays.asList(new Class[] {
            ComponentCatalog.class,
            BOMBundle.class,
            StartInfo.class,
            ActiveOrders.class,
            BankStatus.class,
            // MarketReport.class,
            // RFQBundle.class,
            // SupplierOfferBundle.class,
            // CustomerOrderBundle.class,
            // DeliveryNotice.class,
            FactoryStatus.class,
            PriceReport.class,
            NewDay.class
        });
    }
    
    /**
     * Returns the current day
     * @return
     */
    public int getCurrentDate() {
        return currentDay;
    }

    /**
     * Return days before void
     * @return
     */
    public int getDaysBeforeVoid() {
        return daysBeforeVoid;
    }

    /**
     * Returns the statistics element to perform computation on
     * @return
     */
    public StatisticDataBase getStatistics() {
        return statistics;
    }

    /**
     * Return component catalog
     * @return
     */
    public Catalog getCatalog() {
        return catalog;
    }
}
